import base64
import errno
import logging
import re
import signal
import subprocess
import sys
import time
import webbrowser
import os

from webkitpy.common.config import urls
from webkitpy.common.system import executive
from webkitpy.common.system.path import cygpath
from webkitpy.common.system.executive import Executive, ScriptError
from webkitpy.port import builders, server_process, Driver, DriverOutput

from webkitpy.layout_tests.models.test_configuration import TestConfiguration
from webkitpy.port.base import Port

_log = logging.getLogger(__name__)

DEFAULT_RESULT_DIR = "result"

class OrbisPort(Port):
    port_name = 'orbis'
    @classmethod
    def default_results_directory(self):    
        return DEFAULT_RESULT_DIR

    def default_test_timeout_ms(self):
        return 10 * 1000
       
    def relative_test_filename(self, filename):
        path = filename[len(self.layout_tests_dir()) + 1:]
        return path.replace('\\', '/')

    def _generate_all_test_configurations(self):
        return [TestConfiguration(version=self._version, architecture='x86', build_type=build_type) for build_type in self.ALL_BUILD_TYPES]

    def driver_name(self):
        return "WebKitTestRunnerOrbis.self"

    def _path_to_driver(self):
        return self._build_path(self.driver_name())

    def _build_directory_name(self):
        return self.get_option('platform').upper() + '_' + self.get_option('configuration')

    def layout_tests_dir(self):
        """Return the absolute path to the top of the LayoutTests directory."""
        return  self._build_path().replace(self._build_directory_name(),"fsroot/LayoutTests")

    def _build_path(self, *comps):
        if self.get_option('build_directory'):
           return self._filesystem.join(self.get_option('build_directory'), *comps)
        else:
            relative_path_to_build_directory = 'orbis/WebKitTestRunnerOrbis/' + self._build_directory_name()
            return self._filesystem.join(relative_path_to_build_directory, *comps)

    def _driver_class(self):
       return OrbisDriver

    def _check_file_exists(self, path_to_file, file_description,
                           override_step=None, logging=True):
        """Verify the file is present where expected or log an error.

        Args:
            file_name: The (human friendly) name or description of the file
                you're looking for (e.g., "HTTP Server"). Used for error logging.
            override_step: An optional string to be logged if the check fails.
            logging: Whether or not log the error messages."""
        if not self._filesystem.exists(path_to_file):
            if logging:
                _log.error('Unable to find %s' % file_description)
                _log.error('    at %s' % path_to_file)
                if override_step:
                    _log.error('    %s' % override_step)
                    _log.error('')
            return False
        return True

    def _check_apache_install(self):
        result = self._check_file_exists(self._path_to_apache(), "httpd2.exe")
        result = self._check_file_exists(self._path_to_apache_config_file(), "apache2 config file") and result
        if not result:
            _log.error('    Please install using: "sudo apt-get install apache2 libapache2-mod-php5"')
            _log.error('')
        return result

    def _check_lighttpd_install(self):
        result = self._check_file_exists(
            self._path_to_lighttpd(), "LigHTTPd executable")
        result = self._check_file_exists(self._path_to_lighttpd_php(), "PHP CGI executable") and result
        result = self._check_file_exists(self._path_to_lighttpd_modules(), "LigHTTPd modules") and result
        if not result:
            _log.error('    Please install using: "sudo apt-get install lighttpd php5-cgi"')
            _log.error('')
        return result

    def check_wdiff(self, logging=True):
        result = self._check_file_exists(self._path_to_wdiff(), 'wdiff.exe')
        if not result and logging:
            _log.error('    Please install using: "sudo apt-get install wdiff"')
            _log.error('')
        return result
    
    def _uses_apache(self):
        return False

    def _path_to_apache(self):
        return '/usr/sbin/httpd2.exe'

    def _path_to_apache_config_file(self):
        config_name = 'apache2-httpd.conf'
        return self._filesystem.join(self.layout_tests_dir(), 'http', 'conf', config_name)

    def start_websocket_server(self):
        pass
    
    def _path_to_lighttpd(self):
        return "/usr/sbin/lighttpd.exe"

    def _path_to_lighttpd_modules(self):
        return "/usr/lib/lighttpd"

    def _path_to_lighttpd_php(self):
        return "/usr/bin/php-cgi.exe"        

    def _path_to_wdiff(self):
        return '/usr/bin/wdiff.exe'

    def _startup_time(self):
        return 4.0

    def _execution_time(self):
        # Approximate time for execution of a test 
        # with WebKitTestRunnerOrbis as driver
        return 0.2

    def show_results_html_file(self, results_filename):
        # Convert cygwin path to Windows path
        win_abspath = cygpath(results_filename)

        subprocess.call(["cygstart", win_abspath])

class OrbisDriver(Driver):
    def __init__(self, port, worker_number, pixel_tests, no_timeout=False):
        Driver.__init__(self, port, worker_number, pixel_tests, no_timeout)
        self._driver_tempdir = port._filesystem.mkdtemp(prefix='%s-' % self._port.driver_name())
        # WebKitTestRunner can report back subprocess crashes by printing
        # "#CRASHED - PROCESSNAME".  Since those can happen at any time
        # and ServerProcess won't be aware of them (since the actual tool
        # didn't crash, just a subprocess) we record the crashed subprocess name here.
        self._crashed_process_name = None
        self._crashed_pid = None
        self._proc = None

        # stderr reading is scoped on a per-test (not per-block) basis, so we store the accumulated
        # stderr output, as well as if we've seen #EOF on this driver instance.
        # FIXME: We should probably remove _read_first_block and _read_optional_image_block and
        # instead scope these locally in run_test.
        self.error_from_test = str()
        self.err_seen_eof = False
        self._server_process = None

    # FIXME: This may be unsafe, as python does not guarentee any ordering of __del__ calls
    # I believe it's possible that self._port or self._port._filesystem may already be destroyed.
    def __del__(self):
        self._port._filesystem.rmtree(str(self._driver_tempdir))

    def cmd_line(self, pixel_tests, per_test_args):
        # cmd = self._command_wrapper(self._port.get_option('wrapper'))
        # cmd.append(self._port._path_to_driver())
        # if self._port.get_option('gc_between_tests'):
            # cmd.append('--gc-between-tests')
        # if self._port.get_option('complex_text'):
            # cmd.append('--complex-text')
        # if self._port.get_option('threaded'):
            # cmd.append('--threaded')
        # if self._no_timeout:
            # cmd.append('--no-timeout')
        # # FIXME: We need to pass --timeout=SECONDS to WebKitTestRunner for WebKit2.

        # cmd.extend(self._port.get_option('additional_drt_flag', []))

        # if pixel_tests:
            # cmd.append('--pixel-tests')
        # cmd.extend(per_test_args)

        # #cmd.append('-')
        orbis_ctrl = 'orbis-run /noprogress /c:process /elf '
        path_2_driver = self._port._build_path(self._port.driver_name())
        return orbis_ctrl + path_2_driver

    def _test_shell_command(self,pixel_tests, uri, timeoutms, checksum):
        cmd = uri
        if timeoutms:
           cmd += ' ' + '--timeout' + ' ' + str(timeoutms)
        if pixel_tests:
           cmd += ' ' + '--pixel-tests'        
        return cmd

    def _start(self, pixel_tests, per_test_args):
        server_name = self._port.driver_name()
        if sys.platform == 'cygwin':
           cmd = ["bash.exe"]
        if sys.platform == 'win32':
           cmd = ["cmd.exe"]
        # environment = self._port.setup_environ_for_server(server_name)
        # environment['DYLD_LIBRARY_PATH'] = self._port._build_path()
        # environment['DYLD_FRAMEWORK_PATH'] = self._port._build_path()
        # # # FIXME: We're assuming that WebKitTestRunner checks this DumpRenderTree-named environment variable.
        # environment['DUMPRENDERTREE_TEMP'] = str(self._driver_tempdir)
        # environment['LOCAL_RESOURCE_ROOT'] = self._port.layout_tests_dir()
        self._crashed_process_name = None
        self._crashed_pid = None
        self._server_process = server_process.ServerProcess(self._port, server_name, cmd, None)

    def has_crashed(self):
        if self._server_process is None:
            return False
        if self._crashed_process_name:
            return True
        if self._server_process.has_crashed():
            self._crashed_process_name = self._server_process.name()
            self._crashed_pid = self._server_process.pid()
            return True
        return False

    def _check_for_driver_crash(self, error_line):
        if error_line == "#CRASHED\n":
            # This is used on Windows to report that the process has crashed
            # See http://trac.webkit.org/changeset/65537.
            self._crashed_process_name = self._server_process.name()
            self._crashed_pid = self._server_process.pid()
        elif error_line.startswith("#CRASHED - WebProcess"):
            # WebKitTestRunner uses this to report that the WebProcess subprocess crashed.
            pid = None
            m = re.search('pid (\d+)', error_line)
            if m:
                pid = int(m.group(1))
            self._crashed_process_name = 'WebProcess'
            self._crashed_pid = pid
            # FIXME: delete this after we're sure this code is working :)
            _log.debug('WebProcess crash, pid = %s, error_line = %s' % (str(pid), error_line))
            return True
        return self.has_crashed()

    def _command_from_driver_input(self, driver_input):
        if self.is_http_test(driver_input.test_name):
            command = self.test_to_uri(driver_input.test_name)
        else:
            #command = self._port.abspath_for_test(driver_input.test_name)
            #if sys.platform == 'cygwin':
                #command = cygpath(command)
            command = driver_input.test_name
        if driver_input.should_run_pixel_test:
           if driver_input.image_hash:
            # FIXME: Why the leading quo
            command += "'" + driver_input.image_hash
        return command + "\n"

    def _read_first_block(self, deadline):
        # returns (text_content, audio_content)
        block = self._read_block(deadline)
        if block.content_type == 'audio/wav':
            return (None, block.decoded_content)
        return (block.decoded_content, None)

    def _read_optional_image_block(self, deadline):
        # returns (image, actual_image_hash)
        block = self._read_block(deadline, wait_for_stderr_eof=True)
        if block.content and block.content_type == 'image/png':
            return (block.decoded_content, block.content_hash)
        return (None, block.content_hash)

    def _process_driver_input(self, driver_input):
        test_set = []
        for test in driver_input:
            test_set.append(test.test_name)
        return test_set
    
    def run_multiple_tests(self, driver_input, stop_when_done):
        text = []
        num_tests = len(driver_input)
        start_time = time.time()
        if not self._server_process:
            self._start(driver_input[0].should_run_pixel_test, driver_input[0].args)
        self.error_from_test = str()
        self.err_seen_eof = False
        image = None
        actual_image_hash = None
        # Check the startup time of the Orbis driver. Currently a method returning 
        # a fixed value is used, in the future we can improve this at runtime
        # by executing a dummy run of WebKitTestRunnerOrbis
        time_to_sleep = self._port._startup_time() + (self._port._execution_time()* float(num_tests))

        #To support PSOrbis the path to the driver is returned as the full path to
        #WebKitTestRunnerOrbis.self include Orbis-run /c:process /elf
        driver = self.cmd_line(self._port.get_option('pixel_tests'), [])
        #To provide the options [--timeout],[--pixeltest]
        command  = self._test_shell_command(driver_input[0].should_run_pixel_test,driver, (int(driver_input[0].timeout) * int(num_tests)), driver_input[0].image_hash)
        #combine driver path + test options + Test case file path [*.html]
        test_set = self._process_driver_input(driver_input)
        for test in test_set:
            command += ' ' + '/hostapp/LayoutTests/' + test
        command += '\n'
        _log.debug( "command = %s" % command)
        deadline = start_time + int(driver_input[0].timeout) / 1000.0

        # Bug33130, Adding sleep to avoid process creation error caused by
        # repeatedly invoking orbis-run
        self._server_process.write(command)
        for t in range(0,num_tests):
            self.err_seen_eof = False
            cur_text, audio = self._read_first_block(deadline)  # First block is either text or audio
            if not cur_text and self._server_process.timed_out:
                break
            image, actual_image_hash = self._read_optional_image_block(deadline) 
            text.append(cur_text)
        num_tests_ran = len(text)

        # We may not have read all of the output if an error (crash) occured.
        # Since some platforms output the stacktrace over error, we should
        # dump any buffered error into self.error_from_test.
        # FIXME: We may need to also read stderr until the process dies?
        self.error_from_test += self._server_process.pop_all_buffered_stderr()
        crash_log = ''
        if self.has_crashed():
            crash_log = self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, text[0], self.error_from_test,
                                                  newer_than=start_time)
        driver_output = []
        timeout = self._server_process.timed_out
        if timeout:
            _log.info("\nTimeout: Num of tests returned: %d out of %d" % (num_tests_ran, num_tests))
            for x in range(0, num_tests_ran):
                _log.debug('')
                # Handle the tests that returned before the timeout
                driver_output.append(DriverOutput(text[x].lstrip(), image, actual_image_hash, audio,
                   crash=self.has_crashed(), test_time=time.time() - start_time,
                   timeout=False, error=self.error_from_test,
                   crashed_process_name=self._crashed_process_name,
                   crashed_pid=self._crashed_pid, crash_log=crash_log))
            for x in range(num_tests_ran, num_tests):
                 text = ''
                 driver_output.append(DriverOutput(text.lstrip(), image, actual_image_hash, audio,
                   crash=self.has_crashed(), test_time=time.time() - start_time,
                   timeout=timeout, error=self.error_from_test,
                   crashed_process_name=self._crashed_process_name,
                   crashed_pid=self._crashed_pid, crash_log=crash_log))
            return driver_output, num_tests_ran

        for x in range (0, num_tests):
            driver_output.append(DriverOutput(text[x].lstrip(), image, actual_image_hash, audio,
            crash=self.has_crashed(), test_time=time.time() - start_time,
            timeout=timeout, error=self.error_from_test,
            crashed_process_name=self._crashed_process_name,
            crashed_pid=self._crashed_pid, crash_log=crash_log))
        return driver_output, num_tests_ran

    def run_test(self, driver_input, stop_when_done):
        start_time = time.time()
        if not self._server_process:
            self._start(driver_input.should_run_pixel_test, driver_input.args)
        self.error_from_test = str()
        self.err_seen_eof = False
        time_to_sleep = float(self._port._startup_time()) + float(self._port._execution_time())
        #To support PSOrbis the path to the driver is returned as the full path to
        #WebKitTestRunnerOrbis.self include Orbis-run /c:process /elf
        driver = self.cmd_line(self._port.get_option('pixel_tests'), [])
        #To provide the options [--timeout],[--pixeltest]
        command  = self._test_shell_command(driver_input.should_run_pixel_test,driver, driver_input.timeout, driver_input.image_hash)
        #combine driver path + test options + Test case file path [*.html]
        command += ' ' + '/hostapp/LayoutTests/' + self._command_from_driver_input(driver_input)
        _log.debug( "command = %s" % command)
        deadline = start_time + int(driver_input.timeout) / 1000.0
        self._server_process.write(command)
        text, audio = self._read_first_block(deadline)  # First block is either text or audio
        image, actual_image_hash = self._read_optional_image_block(deadline)  # The second (optional) block is image data.
        # We may not have read all of the output if an error (crash) occured.
        # Since some platforms output the stacktrace over error, we should
        # dump any buffered error into self.error_from_test.
        # FIXME: We may need to also read stderr until the process dies?
        self.error_from_test += self._server_process.pop_all_buffered_stderr()

        crash_log = ''
        if self.has_crashed():
            crash_log = self._port._get_crash_log(self._crashed_process_name, self._crashed_pid, text, self.error_from_test,
                                                  newer_than=start_time)

        timeout = self._server_process.timed_out
#        if timeout:
            #DRT doesn't have a built in timer to abort the test, so we might as well
            #kill the process directly and not wait for it to shut down cleanly (since it may not).
#           self._server_process.kill()

        return DriverOutput(text.lstrip(), image, actual_image_hash, audio,
            crash=self.has_crashed(), test_time=time.time() - start_time,
            timeout=timeout, error=self.error_from_test,
            crashed_process_name=self._crashed_process_name,
            crashed_pid=self._crashed_pid, crash_log=crash_log)

    def _read_header(self, block, line, header_text, header_attr, header_filter=None):
        if line.startswith(header_text) and getattr(block, header_attr) is None:
            value = line.split()[1]
            if header_filter:
                value = header_filter(value)
            setattr(block, header_attr, value)
            return True        
        return False

    def _process_stdout_line(self, block, line):
        if (self._read_header(block, line, 'Content-Type: ', 'content_type')
            or self._read_header(block, line, 'Content-Transfer-Encoding: ', 'encoding')
            or self._read_header(block, line, 'Content-Length: ', '_content_length', int)
            or self._read_header(block, line, 'ActualHash: ', 'content_hash')):
            return
        #Specfic handling for PSOrbis as this comes for each test cases in the stdout
        if ('Load libSce' in line
            or 'Microsoft Windows' in line
            or 'Microsoft Corporation' in line
            or 'orbis-run' in line):
            return
        # Note, we're not reading ExpectedHash: here, but we could.
        # If the line wasn't a header, we just append it to the content.
        block.content += line

    def _strip_eof(self, line):
        if line and line.endswith("#EOF\n"):
            return line[:-5], True
        return line, False

    def _read_block(self, deadline, wait_for_stderr_eof=False):
        block = ContentBlock()
        out_seen_eof = False

        while not self.has_crashed():
            if out_seen_eof and (self.err_seen_eof or not wait_for_stderr_eof):
                break

            if self.err_seen_eof:
                out_line = self._server_process.read_stdout_line(deadline)
                err_line = None
            elif out_seen_eof:
                out_line = None
                err_line = self._server_process.read_stderr_line(deadline)
            else:
                out_line, err_line = self._server_process.read_either_stdout_or_stderr_line(deadline)
                
            if self._server_process.timed_out or self.has_crashed():
                break

            if out_line:
                assert not out_seen_eof
                out_line, out_seen_eof = self._strip_eof(out_line)                
            if err_line:
                assert not self.err_seen_eof
                err_line, self.err_seen_eof = self._strip_eof(err_line)

            if out_line:
                if out_line[-1] != "\n":
                    _log.error("Last character read from DRT stdout line was not a newline!  This indicates either a NRWT or DRT bug.")
                content_length_before_header_check = block._content_length
                self._process_stdout_line(block, out_line)
                
                # FIXME: Unlike HTTP, DRT dumps the content right after printing a Content-Length header.
                # Don't wait until we're done with headers, just read the binary blob right now.
                if content_length_before_header_check != block._content_length:
                    block.content = self._server_process.read_stdout(deadline, block._content_length)

            if err_line:
                if self._check_for_driver_crash(err_line):
                   break
                self.error_from_test += err_line

        block.decode_content()
        return block

    def start(self, pixel_tests, per_test_args):
        if not self._server_process:
            self._start(pixel_tests, per_test_args)

    def stop(self):
        if self._server_process:
            self._rebootorbis()
            self._server_process.stop()
            self._server_process = None

    def _rebootorbis(self):
        if self._server_process:
           if self.has_crashed():
               _log.debug( "crashed...reboot! wait...")
               command = 'orbis-ctrl reboot \n' 
               self._server_process.write(command)
               #time delay for Orbis to complete reboot
               time.sleep(120)

    def _pause_run(self):
        if (self.num_tests % REBOOT_AFTER_NUM_TESTS) == 0:
            self._reboot_required = True
        else:
            # Bug33130, Adding sleep to avoid process creation error caused by
            # repeatedly invoking orbis-run
            time.sleep(9)

class ContentBlock(object):
    def __init__(self):
        self.content_type = None
        self.encoding = None
        self.content_hash = None
        self._content_length = None
        # Content is treated as binary data even though the text output is usually UTF-8.
        self.content = str()  # FIXME: Should be bytearray() once we require Python 2.6.
        self.decoded_content = None

    def decode_content(self):
        if self.encoding == 'base64':
            self.decoded_content = base64.b64decode(self.content)
        else:
            self.decoded_content = self.content
